/*  This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation; either version 2.1 of the License, or (at
    your option) any later version.
    For more details, see the GNU Lesser General Public License (www.fsf.org
    or the COPYING file somewhere in the package)
 */

#include "Utils.hh"

/*! @file src/OSGi/Utils.cc
    @brief Useful functions (non-inline) definition.
    @author @ref Guillaume_Terrissol
    @date 6th February 2010 - 22nd January 2014
    @note This file is distributed under the LGPL license.
    Refer to the file COPYING (or http://www.fsf.org) for more information.
 */

#include <dirent.h>
#include <errno.h>
#include <unistd.h>

#include <algorithm>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <stdexcept>

#include "CGuard.hh"
#include "Exception.hh"

namespace OSGi
{
//------------------------------------------------------------------------------
//                             Operations on Strings
//------------------------------------------------------------------------------

    /*! Removes any whitespace at beginning and end of a string.
        @param pText String to trim
        @return The trimmed string
     */
    std::string& trimString(std::string& pText)
    {
        while(!pText.empty() && std::isspace(pText[pText.length() - 1]))
        {
            pText.resize(pText.length() - 1);
        }
        while(std::isspace(*begin(pText)))
        {
            pText.erase(begin(pText));
        }

        return pText;
    }


    /*! @copydetails trimString(std::string&)
     */
    std::string trimString(std::string&& pText)
    {
        return trimString(pText);
    }


    /*! Splits a string according to a separator.
        @param pText String to split
        @param pSep  Separator used to split @p pText
        @return Result of splitting @p pText by @p pSep
     */
    std::vector<std::string> splitString(const std::string& pText, char pSep)
    {
        std::vector<std::string>    lList;

        for(size_t lPrev = 0, lNext = pText.find(pSep, lPrev);
            lPrev < pText.length();
            lPrev = (lNext != std::string::npos) ? lNext + 1 : std::string::npos, lNext = pText.find(pSep, lPrev))
        {
            lList.push_back(pText.substr(lPrev, lNext - lPrev));
        }

        return lList;
    }


//------------------------------------------------------------------------------
//                              Operations on Paths
//------------------------------------------------------------------------------

    /*! @param pPath Path to check
        @retval true  if @p pPath is an absolute path
        @retval false otherwise
     */
    bool isPathAbsolute(const std::string& pPath)
    {
#if defined(_WIN32)
        return (3 <= pPath.length()) && std::isalpha(pPath[0]) && (pPath.substr(1, 2) == ":/");
#else   // defined(_WIN32)
        return (1 <= pPath.length()) && (pPath[0] == '/');
#endif  // defined(_WIN32)
    }


    /*! @return The current path, with a trailing slash
     */
    std::string workingDirectory()
    {
        static char sBuffer[PATH_MAX + 1] = { '\0' };

        if (char* lWD = getcwd(sBuffer, PATH_MAX))
        {
            size_t  lLen = strlen(lWD);
#if defined(_WIN32)
            std::replace(lWD, lWD + lLen, '\\', '/');
#endif  // defined(_WIN32)
            return std::string{lWD, lLen} + "/";
        }
        else if (errno == ERANGE)
        {
            throw std::length_error{"Working directory too long for MAX_PATH"};
        }
        else
        {
            throw std::runtime_error{std::strerror(errno)};
        }
    }

    
    /*! @param pPath Absolute path of a sub-element in the working directory
        @return The relative path, in @p pPath, from the Working directory
        @note std::invalid_argument if @p pPath doesn't begin with workingDirectory()
     */
    std::string fromWorkingDirectory(const std::string& pPath)
    {
        std::string lWD = workingDirectory();

        if ((lWD.length() < pPath.length()) && (pPath.compare(0, lWD.length(), lWD) == 0))
        {
            // Returns the path after possible '/' (relative path can't start with '/').
            return pPath.substr(pPath.find_first_not_of('/', lWD.length()));
        }

        throw std::invalid_argument{pPath + " doesn't start with " + lWD};
    }


//------------------------------------------------------------------------------
//                           Operations on Directories
//------------------------------------------------------------------------------

    /*! Removes a folder and its children (files, and folders).
        @param pDirName    Name of folder to remove
        @param pRemoveSelf True if @p pDirName shall be erased, False to keep it
        @throw A standard exception will be thrown in case of removing failure (for any element)
        @warning Do @b not call with "/" as an argument
     */
    void removeDirectoryContent(const std::string& pDirName, bool pRemoveSelf)
    {
        CGuard<DIR> lDir{opendir(pDirName.c_str()), closedir};

        if (lDir.get() != nullptr)
        {
            while(dirent* lEntry = readdir(lDir.get()))
            {
                std::string lEntryName = lEntry->d_name;
                if ((lEntryName != ".") && (lEntryName != ".."))
                {
                    std::string lPath = pDirName + "/" + lEntryName;
                    CGuard<DIR> lSubDir{opendir(lPath.c_str()), closedir};
                    if (lSubDir.get() != nullptr)
                    {
                        removeDirectoryContent(lPath, true);
                    }
                    else
                    {
                        if (unlink(lPath.c_str()) != 0)
                        {
                            throw IOError{"Couldn't remove file " + lPath + " : " + strerror(errno)};
                        }
                    }
                }
            }

            if (pRemoveSelf && (rmdir(pDirName.c_str()) != 0))
            {
                throw IOError{"Couldn't remove directory " + pDirName + " : " + strerror(errno)};
            }
        }
    }


    /*! Just a few checks to prevent deleting anything.
        @param pDirName Folder name to check
     */
    bool checkPath(std::string pDirName)
    {
        pDirName = trimString(pDirName);

        return !pDirName.empty() &&
#if defined(_WIN32)
               ((pDirName.length() != 3) || (pDirName.substr(1, 2) != ":/"));
#else   // defined(_WIN32)
               (pDirName != "/");
#endif  // defined(_WIN32)
    }


    /*! Removes a folder and its children (files, and folders).
        @param pDirName    Name of folder to remove
        @throw A standard exception will be thrown in case of removing failure (for any element)
        @warning Do @b not call with "/" as an argument
     */
    void removeDirectory(const std::string& pDirName)
    {
        if (checkPath(pDirName))
        {
            removeDirectoryContent(pDirName, true);
        }
    }


    /*! Removes all elements (files, and folders) of a given folder.
        @param pDirName    Name of folder to empty
        @throw A standard exception will be thrown in case of removing failure (for any element)
        @warning Do @b not call with "/" as an argument
     */
    void emptyDirectory(const std::string& pDirName)
    {
        if (checkPath(pDirName))
        {
            removeDirectoryContent(pDirName, false);
        }
    }
}
